#pragma once
#include <array>
#include <cstdio>
#include <iostream>
#include <ostream>
#include <sstream>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <functional>
#include <cctype>
#include <algorithm>

// 检测 C++23 <print>
#if defined(__cpp_lib_print) && (__cpp_lib_print >= 202207L)
#define KP_HAS_STD_PRINT
#endif

/**
 * @brief C++17 简易格式化打印实现，兼容 C++23 <print>。
 *
 * 本段代码提供：
 * - 检测类型是否可流插入（is_ostream_insertable）
 * - 通用 to_string_any 工具
 * - 轻量级 FormatSpec 结构体，用于承载解析后的格式控制信息
 * - parse_format_spec 函数，用于将格式字符串解析为 FormatSpec
 *
 * 设计初衷：
 * 在缺少 C++23 <print> 的环境中，为 kp::print/println 提供与 std::format
 * 近似的行为，支持常见的对齐、宽度、精度、进制、浮点风格等控制，同时保持
 * 头文件仅依赖、零运行时开销、易于维护。
 */

namespace kp {

#ifndef KP_HAS_STD_PRINT

/**
 * @brief 类型特征：检测 T 是否能通过 operator<< 输出到 std::ostream。
 *
 * 采用 SFINAE 手法，在编译期判断是否存在 `os << t` 表达式。
 * 若存在，则 value 为 true；否则为 false。
 *
 * 设计要点：
 * 1. 使用两个重载的 test 静态函数，一个匹配成功返回 true_type，
 *    另一个兜底返回 false_type。
 * 2. 通过 decltype 与逗号表达式，确保在表达式合法时选中第一个重载。
 * 3. 外部接口仅为 constexpr bool value，零运行时成本。
 *
 * @tparam T 待检测类型
 */
template <class T>
struct is_ostream_insertable {
    /**
     * @brief 匹配成功分支：检测 `std::ostream& << const T&` 是否合法。
     * @return std::true_type 表示可流插入
     */
    template <class U>
    static auto test(int) -> decltype(std::declval<std::ostream&>() << std::declval<const U&>(), std::true_type{});

    /**
     * @brief 匹配失败分支：兜底返回 false_type。
     * @return std::false_type 表示不可流插入
     */
    template <class>
    static auto test(...) -> std::false_type;

    /**
     * @brief 编译期结果，true 表示可流插入，false 表示不可。
     */
    static constexpr bool value = decltype(test<T>(0))::value;
};

/**
 * @brief 将任意可流插入类型转换为 std::string。
 *
 * 内部使用 std::ostringstream 进行格式化，因此支持 operator<< 的重载扩展。
 *
 * @tparam T 待转换类型，必须满足 is_ostream_insertable<T>::value == true
 * @param v 常量引用，待转换对象
 * @return 转换后的 std::string
 * @note 若 T 不可流插入，将在编译期触发 static_assert 报错
 */
template <class T>
inline std::string to_string_any(const T& v) {
    static_assert(is_ostream_insertable<T>::value, "kp::print argument must be stream-insertable");
    std::ostringstream oss;
    oss << v;
    return oss.str();
}

/**
 * @brief 格式控制说明符的解析结果载体。
 *
 * 对应 std::format 风格字符串中
 * [fill][align][sign]['#']['0'][width]['.' precision][type] 的解析结果。
 *
 * 设计初衷：
 * 1. 使用 POD 结构体，保证内存布局紧凑，便于拷贝/传参。
 * 2. 所有成员均赋予合理默认值，避免未初始化行为。
 * 3. 与 parse_format_spec 成对使用，职责单一。
 *
 * 成员含义：
 * - fill     填充字符，默认空格
 * - align    对齐方式，'<' 左、'>' 右、'^' 居中，默认右对齐
 * - sign     符号控制，'+' 强制正号、' ' 正号留空，默认 0 表示无符号控制
 * - alt      alternate 形式，true 时对于 16/8 进制加前缀 0x/0X/0
 * - zero     零填充标志，true 时用 '0' 填充，优先级低于 align
 * - width    字段宽度，0 表示无宽度限制
 * - precision 精度，-1 表示默认精度
 * - type     类型字符，如 d/x/X/o/f/F/e/E/g/G/s/c，0 表示默认
 */
struct FormatSpec {
    char fill = ' ';   ///< 填充字符
    char align = '>';  ///< 对齐方向
    char sign = 0;     ///< '+' 或 ' '，0 表示不控制符号
    bool alt = false;  ///< '#' 标志位
    bool zero = false; ///< '0' 标志位
    int width = 0;     ///< 字段宽度
    int precision = -1;///< 精度，-1 表示默认
    char type = 0;     ///< 类型字符，0 表示默认
};

/**
 * @brief 将格式说明字符串解析为 FormatSpec 结构体。
 *
 * 语法近似 std::format，支持子集：
 * [fill][align][sign]['#']['0'][width]['.' precision][type]
 *
 * 解析流程：
 * 1. fill+align 或仅 align
 * 2. sign
 * 3. '#'
 * 4. '0'
 * 5. width（数字序列）
 * 6. '.' precision（数字序列）
 * 7. type（单字符）
 *
 * 设计要点：
 * - 采用手工递归下降，避免正则依赖，保持头文件轻量。
 * - 对非法字符静默跳过，保证健壮性。
 * - 所有数值采用 int 存储，宽度/精度上限受 int 最大值限制。
 *
 * @param spec 格式说明字符串，如 ":0>+#10.3f"
 * @return 解析后的 FormatSpec 对象
 */
inline FormatSpec parse_format_spec(std::string_view spec) {
    FormatSpec fs{};              ///< 初始化默认值
    size_t i = 0;                 ///< 当前扫描位置

    /* 1. 解析 fill + align 或仅 align */
    if (i + 1 < spec.size() && (spec[i + 1] == '<' || spec[i + 1] == '>' || spec[i + 1] == '^')) {
        fs.fill = spec[i];        ///< 第一个字符作为填充
        fs.align = spec[i + 1];   ///< 第二个字符作为对齐
        i += 2;
    } else if (i < spec.size() && (spec[i] == '<' || spec[i] == '>' || spec[i] == '^')) {
        fs.align = spec[i];       ///< 仅对齐字符
        ++i;
    }

    /* 2. 解析 sign */
    if (i < spec.size() && (spec[i] == '+' || spec[i] == ' ')) {
        fs.sign = spec[i++];
    }

    /* 3. 解析 '#' */
    if (i < spec.size() && spec[i] == '#') {
        fs.alt = true;
        ++i;
    }

    /* 4. 解析 '0' */
    if (i < spec.size() && spec[i] == '0') {
        fs.zero = true;
        ++i;
    }

    /* 5. 解析 width */
    int w = 0;
    bool w_any = false;
    while (i < spec.size() && std::isdigit(static_cast<unsigned char>(spec[i]))) {
        w = w * 10 + (spec[i] - '0');
        ++i;
        w_any = true;
    }
    if (w_any) fs.width = w;

    /* 6. 解析 precision */
    if (i < spec.size() && spec[i] == '.') {
        ++i;
        int p = 0;
        bool p_any = false;
        while (i < spec.size() && std::isdigit(static_cast<unsigned char>(spec[i]))) {
            p = p * 10 + (spec[i] - '0');
            ++i;
            p_any = true;
        }
        fs.precision = p_any ? p : 0; ///< 若仅写 "." 则精度为 0
    }

    /* 7. 解析 type */
    if (i < spec.size()) {
        char t = spec[i];
        switch (t) {
            case 'd': case 'x': case 'X': case 'o':
            case 'f': case 'F': case 'e': case 'E':
            case 'g': case 'G': case 's': case 'c':
                fs.type = t;
                break;
            default:
                break; ///< 未识别则保留默认
        }
    }
    return fs;
}
/**
 * @brief 对普通字符串执行对齐与填充。
 *
 * 本函数仅处理“宽度”与“对齐”需求，不解析数值符号、进制前缀等。
 * 当指定宽度小于等于 0 或原字符串长度已达标时，直接返回原串。
 * 否则依据 align（'<', '^', '>'）与 fill/zero 标志构造填充结果。
 *
 * 设计要点：
 * 1. 零填充（zero=true）仅影响填充字符，不改变对齐逻辑。
 * 2. 居中对齐采用“左多右少”策略，保证总宽度精确。
 * 3. 函数内部不修改入参，所有中间结果通过值返回，线程安全。
 *
 * @param s     待对齐的原始字符串（按值传递，允许内部 move 优化）
 * @param fs    已解析的格式控制说明
 * @return      对齐并填充后的新字符串
 */
inline std::string apply_align(std::string s, const FormatSpec& fs) {
    if (fs.width <= 0) return s;
    const size_t w = static_cast<size_t>(fs.width);
    if (s.size() >= w) return s;
    size_t pad = w - s.size();
    char fill = fs.zero ? '0' : fs.fill;
    if (fs.align == '<') {
        return s + std::string(pad, fill);
    } else if (fs.align == '^') {
        size_t left = pad / 2;
        size_t right = pad - left;
        return std::string(left, fill) + s + std::string(right, fill);
    } else {
        return std::string(pad, fill) + s;
    }
}

/**
 * @brief 对“已拆分的数值字符串”执行对齐与填充。
 *
 * 与 apply_align 不同，本函数额外处理零填充时“0 必须紧跟在符号/前缀之后”的语义。
 * 例如：printf("%#010x", 15) -> "0x0000000f"，其中 0 不能覆盖 "0x"。
 *
 * 设计初衷：
 * 1. 将符号前缀、进制前缀、数字本体拆分传入，避免二次解析。
 * 2. 当 zero=true 且右对齐时，优先在“前缀之后、数字本体之前”插入零，而非简单左填充。
 * 3. 其余对齐方式与 apply_align 保持一致，确保代码复用。
 *
 * @param sign_prefix  符号部分，如 "-", "+", " "
 * @param alt_prefix   进制前缀，如 "0x", "0X", "0"
 * @param body         数字本体，已转换完成，不含符号与前缀
 * @param fs           已解析的格式控制说明
 * @return             对齐并填充后的最终字符串
 */
inline std::string apply_align_numeric(const std::string& sign_prefix,
                                       const std::string& alt_prefix,
                                       const std::string& body,
                                       const FormatSpec& fs) {
    std::string core = sign_prefix + alt_prefix + body;
    if (fs.width <= 0 || core.size() >= static_cast<size_t>(fs.width)) return core;
    size_t pad = static_cast<size_t>(fs.width) - core.size();
    char fill = fs.zero ? '0' : fs.fill;
    if (fs.zero && fs.align == '>') {
        // 零填充置于符号/前缀之后
        return sign_prefix + alt_prefix + std::string(pad, '0') + body;
    }
    if (fs.align == '<') {
        return core + std::string(pad, fill);
    } else if (fs.align == '^') {
        size_t left = pad / 2;
        size_t right = pad - left;
        return std::string(left, fill) + core + std::string(right, fill);
    } else {
        return std::string(pad, fill) + core;
    }
}

/**
 * @brief 将无符号 64 位整数按指定进制转换为字符串。
 *
 * 支持 2~36 进制，通过 digits 数组大小隐式约束。
 * 输出字母部分可通过 upper 参数控制大小写。
 *
 * 设计要点：
 * 1. 特殊处理 0 值，直接返回 "0"，避免后续空串。
 * 2. 采用“先收集后翻转”策略，避免头部插入开销。
 * 3. 所有运算使用 unsigned long long，保证大数安全。
 *
 * @param v     待转换的无符号整数
 * @param base  目标进制，范围 2~36
 * @param upper 是否使用大写字母（仅影响 >9 的数字）
 * @return      转换后的字符串，不含前缀
 */
inline std::string int_to_base(unsigned long long v, int base, bool upper) {
    const char* digits = upper ? "0123456789ABCDEF" : "0123456789abcdef";
    if (v == 0) return std::string("0");
    std::string s;
    while (v > 0) {
        s.push_back(digits[v % static_cast<unsigned long long>(base)]);
        v /= static_cast<unsigned long long>(base);
    }
    std::reverse(s.begin(), s.end());
    return s;
}

/**
 * @brief 将任意类型对象按格式说明符转换为最终字符串。
 *
 * 本函数为 kp::print/println 的核心实现，承担“类型→文本”的最后一公里。
 * 内部采用编译期 if constexpr 进行类型分发，覆盖：
 * 1. 整型（含进制、符号、前缀、零填充）
 * 2. 浮点（精度、风格、大写、强制小数点）
 * 3. bool（文本/数字双模式）
 * 4. char/unsigned char/signed char（字符/ASCII 码双模式）
 * 5. 可转换为 std::string_view 的类型（精度截断）
 * 6. 其他可流插入类型（兜底）
 *
 * 设计初衷：
 * - 单函数入口，避免用户层多重重载。
 * - 所有分支在编译期展开，零运行时多态开销。
 * - 与 FormatSpec 解析结果紧密配合，保证行为与 std::format 子集一致。
 *
 * @tparam T 待格式化类型
 * @param v  常量引用，待格式化对象
 * @param spec_sv 格式说明字符串（不含外层“{}”），如 "0>+#10.3f"
 * @return 格式化后的 std::string，可直接输出
 */
template <class T>
inline std::string format_value(const T& v, std::string_view spec_sv) {
    FormatSpec fs = parse_format_spec(spec_sv);

    /* --------------- 整型分支 --------------- */
    if constexpr (std::is_integral<T>::value && !std::is_same<T, bool>::value &&
                  !std::is_same<T, char>::value && !std::is_same<T, signed char>::value &&
                  !std::is_same<T, unsigned char>::value) {
        bool upper = (fs.type == 'X');   // 16 进制大写
        int base = 10;
        if (fs.type == 'x' || fs.type == 'X') base = 16;
        else if (fs.type == 'o') base = 8;
        // 二进制 'b' 暂不支持，保持最小子集

        bool neg = false;
        unsigned long long uv = 0ULL;

        /* 有符号→无符号的健壮转换：避免 UB 并兼容 LLONG_MIN */
        if constexpr (std::is_signed<T>::value) {
            long long ll = static_cast<long long>(v);
            if (ll < 0) {
                neg = true;
                // 处理 LLONG_MIN：先取 -(ll+1) 再补 1，避免溢出
                uv = static_cast<unsigned long long>(-(ll + 1));
                uv += 1ULL;
            } else {
                uv = static_cast<unsigned long long>(ll);
            }
        } else {
            uv = static_cast<unsigned long long>(v);
        }

        std::string digits = int_to_base(uv, base, upper);
        std::string alt_prefix;
        if (fs.alt) {               // '#' 需要前缀
            if (fs.type == 'x')      alt_prefix = "0x";
            else if (fs.type == 'X') alt_prefix = "0X";
            else if (fs.type == 'o') alt_prefix = "0";
        }
        std::string sign_prefix;
        if (neg) sign_prefix = "-";
        else if (fs.sign) sign_prefix.push_back(fs.sign); // '+' 或 ' '

        /* 使用专用数值对齐函数，保证零填充在符号/前缀之后 */
        return apply_align_numeric(sign_prefix, alt_prefix, digits, fs);
    }
    /* --------------- 浮点分支 --------------- */
    else if constexpr (std::is_floating_point<T>::value) {
        std::ostringstream oss;
        if (fs.precision >= 0) oss.precision(fs.precision);

        /* 按 type 设置浮点风格 */
        if (fs.type == 'f' || fs.type == 'F') {
            oss.setf(std::ios::fixed, std::ios::floatfield);
        } else if (fs.type == 'e' || fs.type == 'E') {
            oss.setf(std::ios::scientific, std::ios::floatfield);
        } else {
            oss.setf(std::ios::fmtflags(0), std::ios::floatfield); // 默认 general
        }
        if (fs.alt) oss.setf(std::ios::showpoint);      // '#' 强制小数点
        if (fs.type == 'F' || fs.type == 'E' || fs.type == 'G') oss.setf(std::ios::uppercase);

        oss << v;
        std::string body = oss.str();

        /* 分离符号与数字本体，便于后续对齐 */
        std::string sign_prefix;
        if (!body.empty() && body[0] == '-') {
            sign_prefix = "-";
            body.erase(0, 1);
        } else if (fs.sign) {
            sign_prefix.push_back(fs.sign);
        }
        return apply_align_numeric(sign_prefix, std::string()/*无前缀*/, body, fs);
    }
    /* --------------- bool 分支 --------------- */
    else if constexpr (std::is_same<T, bool>::value) {
        std::string s;
        if (fs.type == 'd') s = v ? "1" : "0";   // 数字模式
        else                s = v ? "true" : "false";
        return apply_align(std::move(s), fs);
    }
    /* --------------- 字符分支 --------------- */
    else if constexpr (std::is_same<T, char>::value || std::is_same<T, signed char>::value ||
                       std::is_same<T, unsigned char>::value) {
        if (fs.type == 'd') {
            int code = static_cast<int>(v);
            return apply_align(std::to_string(code), fs);
        } else {
            std::string s(1, static_cast<char>(v));
            return apply_align(std::move(s), fs);
        }
    }
    /* --------------- 字符串分支 --------------- */
    else if constexpr (std::is_convertible<T, std::string_view>::value) {
        std::string s = std::string(std::string_view(v));
        if (fs.precision >= 0 && static_cast<size_t>(fs.precision) < s.size()) {
            s.resize(static_cast<size_t>(fs.precision)); // 精度截断
        }
        return apply_align(std::move(s), fs);
    }
    /* --------------- 兜底分支 --------------- */
    else {
        std::string s = to_string_any(v); // 依赖 operator<<
        return apply_align(std::move(s), fs);
    }
}

/**
 * @brief 无参重载：仅返回原始格式字符串本身。
 *
 * 用于 kp::vformat(fmt) 的快捷路径，避免额外分配。
 *
 * @param fmt 格式字符串
 * @return 原样返回的 std::string
 */
inline std::string vformat(std::string_view fmt) {
    return std::string(fmt);
}

/**
 * @brief 将格式字符串与实参列表展开为最终文本，供 kp::print/println 使用。
 *
 * 本函数是 kp 格式化系统的“引擎”，承担以下职责：
 * 1. 扫描 fmt，识别转义字符“{{”“}}”与占位符“{}”“{:spec}”。
 * 2. 按出现顺序将实参逐个绑定到占位符，不支持手动指定索引。
 * 3. 对每一个占位符提取可选的格式说明（冒号后的内容），
 *    并调用 format_value 完成“实参→文本”的转换。
 * 4. 将转换结果追加到输出字符串，最终一次性返回，减少内存分配。
 *
 * 设计要点：
 * - 使用 std::function 数组将实参捕获为“格式化闭包”，
 *   保证后续扫描阶段仅需 O(1) 索引，无需再次展开参数包。
 * - 对 fmt 长度做保守预分配（+32），避免频繁扩容。
 * - 遇到不完整的“{”或“}”时，按字面量输出，保证健壮性。
 * - 整个流程无异常抛出，若用户提供的格式说明非法，
 *   由 format_value 内部静默降级。
 *
 * @tparam Args 实参类型包，需与 fmt 中占位符数量严格一致
 * @param fmt   格式字符串，支持“{{”“}}”转义与“{}”“{:spec}”占位
 * @param args  实参列表，按出现顺序与占位符一一对应
 * @return      格式化后的完整字符串
 * @note        若占位符数量多于实参，多余占位符会被留空；反之实参被忽略
 */
template <class... Args>
inline std::string vformat(std::string_view fmt, const Args&... args) {
    // 将每个实参捕获为可调用对象，供后续按索引消费
    std::array<std::function<std::string(std::string_view)>, sizeof...(Args)> formatters{
        ([&args](std::string_view spec) { return format_value(args, spec); })...
    };
    std::string out;
    out.reserve(fmt.size() + 32);  // 保守预分配，减少扩容

    size_t pos = 0;   // 扫描指针
    size_t ai = 0;    // 实参索引，按出现顺序递增
    while (pos < fmt.size()) {
        char c = fmt[pos];
        if (c == '{') {
            if (pos + 1 < fmt.size() && fmt[pos + 1] == '{') {
                // 转义“{{”
                out.push_back('{');
                pos += 2;
            } else {
                size_t end = fmt.find('}', pos + 1);
                if (end == std::string_view::npos) {
                    // 找不到闭合“}”，按字面量输出“{”
                    out.push_back('{');
                    ++pos;
                } else {
                    // 提取冒号后的格式说明（若存在）
                    std::string_view inside = fmt.substr(pos + 1, end - (pos + 1));
                    std::string_view spec;
                    if (!inside.empty() && inside[0] == ':') {
                        spec = inside.substr(1);
                    } else {
                        spec = std::string_view{};
                    }
                    // 若实参未耗尽，则调用对应闭包进行格式化
                    if (ai < formatters.size()) out.append(formatters[ai++](spec));
                    pos = end + 1;
                }
            }
        } else if (c == '}') {
            if (pos + 1 < fmt.size() && fmt[pos + 1] == '}') {
                // 转义“}}”
                out.push_back('}');
                pos += 2;
            } else {
                // 单独的“}”，按字面量输出
                out.push_back('}');
                ++pos;
            }
        } else {
            // 普通字符直接追加
            out.push_back(c);
            ++pos;
        }
    }
    return out;
}

/**
 * @brief 将格式化结果输出至标准输出（std::cout）。
 *
 * 等价于 `std::cout << kp::vformat(fmt, args...);`，
 * 但避免中间临时字符串的二次拷贝（依赖 RVO）。
 *
 * @tparam Args 实参类型包
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void print(std::string_view fmt, const Args&... args) {
    std::cout << vformat(fmt, args...);
}

/**
 * @brief 将格式化结果输出至标准输出，并追加换行符。
 *
 * 等价于 `kp::print(fmt, args...); std::cout << '\n';`，
 * 但减少一次 IO 调用。
 *
 * @tparam Args 实参类型包
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void println(std::string_view fmt, const Args&... args) {
    std::cout << vformat(fmt, args...) << '\n';
}

/**
 * @brief 将格式化结果输出至任意 std::ostream。
 *
 * 为支持输出到文件流、字符串流等自定义目标提供统一入口。
 *
 * @tparam Args 实参类型包
 * @param os    目标输出流
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void print(std::ostream& os, std::string_view fmt, const Args&... args) {
    os << vformat(fmt, args...);
}

/**
 * @brief 将格式化结果输出至任意 std::ostream，并追加换行符。
 *
 * @tparam Args 实参类型包
 * @param os    目标输出流
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void println(std::ostream& os, std::string_view fmt, const Args&... args) {
    os << vformat(fmt, args...) << '\n';
}

/**
 * @brief 将格式化结果输出至 C FILE*。
 *
 * 采用 fwrite 批量写入，避免字符级 IO 开销。
 * 若 f 为 nullptr，则静默跳过，保证健壮性。
 *
 * @tparam Args 实参类型包
 * @param f     目标 FILE*，允许为 nullptr
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void print(std::FILE* f, std::string_view fmt, const Args&... args) {
    std::string s = vformat(fmt, args...);
    if (f) std::fwrite(s.data(), 1, s.size(), f);
}

/**
 * @brief 将格式化结果输出至 C FILE*，并追加换行符。
 *
 * 内部一次性拼接换行符，减少一次系统调用。
 *
 * @tparam Args 实参类型包
 * @param f     目标 FILE*，允许为 nullptr
 * @param fmt   格式字符串
 * @param args  实参列表
 */
template <class... Args>
inline void println(std::FILE* f, std::string_view fmt, const Args&... args) {
    std::string s = vformat(fmt, args...);
    s.push_back('\n');
    if (f) std::fwrite(s.data(), 1, s.size(), f);
}

#else
// 直接转发到 C++23 标准库 <print>
#include <print>

template <class... Args>
inline void print(std::string_view fmt, Args&&... args) {
    std::print(fmt, std::forward<Args>(args)...);
}

template <class... Args>
inline void println(std::string_view fmt, Args&&... args) {
    std::println(fmt, std::forward<Args>(args)...);
}

template <class... Args>
inline void print(std::ostream& os, std::string_view fmt, Args&&... args) {
    std::print(os, fmt, std::forward<Args>(args)...);
}

template <class... Args>
inline void println(std::ostream& os, std::string_view fmt, Args&&... args) {
    std::println(os, fmt, std::forward<Args>(args)...);
}

template <class... Args>
inline void print(std::FILE* f, std::string_view fmt, Args&&... args) {
    std::print(f, fmt, std::forward<Args>(args)...);
}

template <class... Args>
inline void println(std::FILE* f, std::string_view fmt, Args&&... args) {
    std::println(f, fmt, std::forward<Args>(args)...);
}
#endif // KP_HAS_STD_PRINT

} // namespace kp
